package io.donlon.android.remotecontrol;

import android.os.Handler;
import android.os.Looper;
import android.os.StrictMode;
import android.text.TextUtils;
import android.widget.*;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.gcssloop.widget.RockerView;

public class MainActivity extends AppCompatActivity implements Connection.CommandReceiver {
    private static final int PROGRESS_BAR_MAX = 200;

    private static final float ANGLE_P_FROM = 0.f;
    private static final float ANGLE_P_TO = .3f;

    private static final float ANGLE_D_FROM = 0.f;
    private static final float ANGLE_D_TO = .03f;

    private static final float SPEED_P_FROM = 0.f;
    private static final float SPEED_P_TO = -.2f;

    @BindView(R.id.editTextHost)
    EditText editTextHost;

    @BindView(R.id.editTextPort)
    EditText editTextPort;

    @BindView(R.id.buttonConnect)
    Button buttonConnect;

    @BindView(R.id.textViewStatus)
    TextView textViewStatus;

    @BindView(R.id.buttonSend)
    Button buttonSend;

    @BindView(R.id.buttonPing)
    Button buttonPing;

    @BindView(R.id.buttonFlush)
    Button buttonFlush;

    @BindView(R.id.editTextCommand)
    EditText editTextCommand;

    @BindView(R.id.rocker)
    RockerView rockerView;

    @BindView(R.id.switchPower)
    Switch switchPower;

    @BindView(R.id.switchTracking)
    Switch switchTracking;

    @BindView(R.id.textViewSpeedInfo)
    TextView textViewSpeedInfo;

    @BindView(R.id.textViewRemoteSpeedL)
    TextView textViewRemoteSpeedL;

    @BindView(R.id.textViewRemoteSpeedR)
    TextView textViewRemoteSpeedR;

    @BindView(R.id.textViewIRStatus)
    TextView textViewIRStatus;

    // Parameters

    @BindView(R.id.textViewAngleP)
    TextView textViewAngleP;

    @BindView(R.id.seekBarAngleP)
    SeekBar seekBarAngleP;

    @BindView(R.id.textViewAngleD)
    TextView textViewAngleD;

    @BindView(R.id.seekBarAngleD)
    SeekBar seekBarAngleD;

    @BindView(R.id.textViewSpeedP)
    TextView textViewSpeedP;

    @BindView(R.id.seekBarSpeedP)
    SeekBar seekBarSpeedP;

    private long lastParameterSentTime = 0;
    private long lastSpeedSentTime = 0;

    Handler handler = new Handler(Looper.getMainLooper());

    SeekBar.OnSeekBarChangeListener onSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() {
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            MainActivity.this.onProgressChanged(seekBar, progress, fromUser);
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
        }
    };

    private Runnable querySpeedRunnable = new Runnable() {
        @Override
        public void run() {
            Connection.postCommand("q:speed;q:ir;", "ir");

            handler.postDelayed(this, 300);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (android.os.Build.VERSION.SDK_INT > 9) {
            StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder(StrictMode.getThreadPolicy())
                    .permitAll()
                    .build();
            StrictMode.setThreadPolicy(policy);
        }
        ButterKnife.bind(this);
        init();
    }

    private float getSavedParameterProgress(ParameterType type) {
        return getParameterProgressInverse(type, StorageUtils.getFloat(type.toString(), 0));
    }

    private void saveParameterProgress(ParameterType type, float value) {
        StorageUtils.setFloat(type.toString(), value);
    }

    private void saveParameterProgress(ParameterType type, SeekBar seekBar) {
        float value = getParameterProgress(type, (float) seekBar.getProgress() / (float) PROGRESS_BAR_MAX);
        saveParameterProgress(type, value);
    }

    private void init() {
        buttonConnect.setOnClickListener(v -> connectToServer());
        buttonSend.setOnClickListener(v -> sendCommand());
        buttonPing.setOnClickListener(v -> Connection.postCommand("ping"));
        buttonFlush.setOnClickListener(v -> flushParameters());
        rockerView.setListener(this::onRockerViewEvent);
        rockerView.setRefreshCycle(1000 / 60);
        rockerView.setCallbackCycle(1000 / 20);
        switchPower.setChecked(StorageUtils.getBoolean("power", true));
        switchTracking.setChecked(StorageUtils.getBoolean("tracking", false));
        switchPower.setOnCheckedChangeListener((v, isChecked) -> {
            Connection.postCommand("power=" + (isChecked ? "1" : "0"));
            StorageUtils.setBoolean("power", isChecked);
        });
        switchTracking.setOnCheckedChangeListener((v, isChecked) -> {
            Connection.postCommand("tracking=" + (isChecked ? "1" : "0"));
            StorageUtils.setBoolean("tracking", isChecked);
        });

        initSeekBars();

        editTextHost.setText("192.168.4.1");
        editTextPort.setText("9331");
    }

    private void initSeekBars() {
        seekBarAngleP.setMax(PROGRESS_BAR_MAX);
        seekBarAngleD.setMax(PROGRESS_BAR_MAX);
        seekBarSpeedP.setMax(PROGRESS_BAR_MAX);

        float angleP = StorageUtils.getFloat(ParameterType.AngleP.toString(), 0);
        float angleD = StorageUtils.getFloat(ParameterType.AngleD.toString(), 0);
        float speedP = StorageUtils.getFloat(ParameterType.SpeedP.toString(), 0);

        seekBarAngleP.setProgress((int) (getParameterProgressInverse(ParameterType.AngleP, angleP) * (float) PROGRESS_BAR_MAX));
        seekBarAngleD.setProgress((int) (getParameterProgressInverse(ParameterType.AngleD, angleD) * (float) PROGRESS_BAR_MAX));
        seekBarSpeedP.setProgress((int) (getParameterProgressInverse(ParameterType.SpeedP, speedP) * (float) PROGRESS_BAR_MAX));

        seekBarAngleP.setOnSeekBarChangeListener(onSeekBarChangeListener);
        seekBarAngleD.setOnSeekBarChangeListener(onSeekBarChangeListener);
        seekBarSpeedP.setOnSeekBarChangeListener(onSeekBarChangeListener);

        // TODO: ...
        textViewAngleP.setText(String.format("Angle-P: %.4f", angleP));
        textViewAngleD.setText(String.format("Angle-D: %.4f", angleD));
        textViewSpeedP.setText(String.format("Speed-P: %.4f", speedP));
    }

    private void connectToServer() {
        String host = editTextHost.getText().toString();
        String portString = editTextPort.getText().toString();
        int port;
        try {
            port = Integer.parseInt(portString);
        } catch (NumberFormatException e) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this)
                    .setTitle("Error")
                    .setMessage("Port should be integer");
            builder.create().show();
            return;
        }
        boolean result = Connection.connect(host, port);
        if (!result) {
            textViewStatus.setText("Failed");
            return;
        }
        textViewStatus.setText(String.format("Connected to %s:%d", host, port));
        flushParameters();

        Connection connection = Connection.getConnection();
        connection.setCommandReceiver(this);
        connection.setConnectionClosedListener(() -> {
            textViewStatus.setText("Connection closed.");
            handler.removeCallbacks(querySpeedRunnable);
        });
        handler.postDelayed(querySpeedRunnable, 1000);
    }

    @Override
    public void onCommandReceived(@NonNull String cmd) {
        int i = cmd.indexOf(':');
        if (i < 0) {
            i = cmd.indexOf('=');
        }
        if (i >= 0) { // foo:bar
            String item = cmd.substring(0, i);
            String valueString = cmd.substring(i + 1);
            switch (item) {
                case "speed_l":
                case "speed_r":
                    float speed = Float.parseFloat(valueString);
                    if (item.equals("speed_l")) {
                        textViewRemoteSpeedL.setText(String.format("Speed L: %.3f", speed));
                    } else {
                        textViewRemoteSpeedR.setText(String.format("Speed R: %.3f", speed));
                    }
                    break;
                case "ir":
                    textViewIRStatus.setText(String.format("IR: %s", valueString));
                case "logging":
                    break;
            }
        } else {
            AlertDialog.Builder builder = new AlertDialog.Builder(this)
                    .setTitle("Received message")
                    .setMessage(cmd);
            builder.create().show();
        }
    }

    private void flushParameters() {
        float percentage1 = (float) seekBarAngleP.getProgress() / (float) PROGRESS_BAR_MAX;
        float percentage2 = (float) seekBarAngleD.getProgress() / (float) PROGRESS_BAR_MAX;
        float percentage3 = (float) seekBarSpeedP.getProgress() / (float) PROGRESS_BAR_MAX;
        float value1 = getParameterProgress(ParameterType.AngleP, percentage1);
        float value2 = getParameterProgress(ParameterType.AngleD, percentage2);
        float value3 = getParameterProgress(ParameterType.SpeedP, percentage3);
        Connection.postCommand(String.format("power=%d;tracking=%d;p=%.3f;d=%.3f;speed_p=%.3f;",
                switchPower.isChecked() ? 1 : 0,
                switchTracking.isChecked() ? 1 : 0,
                value1, value2, value3));

        saveParameterRunnable.run();
    }

    private void sendCommand() {
        if (!Connection.isConnected()) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this)
                    .setTitle("Error")
                    .setMessage("Socket is not connected");
            builder.create().show();
            return;
        }
        String cmd = editTextCommand.getText().toString();
        if (TextUtils.isEmpty(cmd)) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this)
                    .setTitle("Error")
                    .setMessage("Command should not be empty");
            builder.create().show();
            return;
        }
        Connection.getConnection().sendCommand(cmd);
    }

    private float lastX = -1;
    private float lastY = -1;

    private void onRockerViewEvent(int eventType, float x, float y) {
        if (eventType == RockerView.EVENT_CLOCK) {
            if (x == lastX && y == lastY) {
                return;
            }
            float speed = y * 50.f;
            float speedDiff = -x * 5.f;

            long now = System.currentTimeMillis();
            if (now - lastSpeedSentTime <= 100) {
                return;
            }
            lastSpeedSentTime = now;

            Connection.postCommand(String.format("speed=%.4f;sp_diff=%.4f;", speed, speedDiff));

            runOnUiThread(() -> textViewSpeedInfo.setText(String.format("Speed: %.2f\nDiff: %.2f", speed, speedDiff)));

            lastX = x;
            lastY = y;
        }
    }

    private float getParameterProgress(ParameterType type, float progress) {
        float form;
        float to;
        switch (type) {
            case AngleP:
                form = ANGLE_P_FROM;
                to = ANGLE_P_TO;
                break;
            case AngleD:
                form = ANGLE_D_FROM;
                to = ANGLE_D_TO;
                break;
            case SpeedP:
                form = SPEED_P_FROM;
                to = SPEED_P_TO;
                break;
            default:
                return 0.f;
        }
        return form + (to - form) * progress;
    }

    private float getParameterProgressInverse(ParameterType type, float progress) {
        float form;
        float to;
        switch (type) {
            case AngleP:
                form = ANGLE_P_FROM;
                to = ANGLE_P_TO;
                break;
            case AngleD:
                form = ANGLE_D_FROM;
                to = ANGLE_D_TO;
                break;
            case SpeedP:
                form = SPEED_P_FROM;
                to = SPEED_P_TO;
                break;
            default:
                return 0.f;
        }
        return (progress - form) / (to - form);
    }

    Runnable saveParameterRunnable = new Runnable() {
        @Override
        public void run() {
            saveParameterProgress(ParameterType.AngleP, seekBarAngleP);
            saveParameterProgress(ParameterType.AngleD, seekBarAngleD);
            saveParameterProgress(ParameterType.SpeedP, seekBarSpeedP);
        }
    };

    private void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        TextView tv;
        String name;
        String parameterName;
        ParameterType type;
        if (seekBar == seekBarAngleP) {
            tv = this.textViewAngleP;
            name = "Angle-P";
            type = ParameterType.AngleP;
            parameterName = "p";
        } else if (seekBar == seekBarAngleD) {
            tv = this.textViewAngleD;
            name = "Angle-D";
            type = ParameterType.AngleD;
            parameterName = "d";
        } else if (seekBar == seekBarSpeedP) {
            tv = this.textViewSpeedP;
            name = "Speed-P";
            type = ParameterType.SpeedP;
            parameterName = "speed_p";
        } else {
            return;
        }
        float percentage = (float) progress / (float) PROGRESS_BAR_MAX;
        float value = getParameterProgress(type, percentage);
        tv.setText(String.format("%s: %.4f", name, value));

        long now = System.currentTimeMillis();
        if (now - lastParameterSentTime > 50) {
            Connection.postCommand(String.format("%s=%.3f;", parameterName, value));
            lastParameterSentTime = now;
        }

        handler.removeCallbacks(saveParameterRunnable);
        handler.postDelayed(saveParameterRunnable, 500);
    }

    enum ParameterType {
        AngleP,
        AngleD,
        SpeedP,
    }
}